Jerry's Log

docker - build

contents

docker build 프로세스는 Dockerfile에 정의된 사람이 읽을 수 있는 명령어와 관련 빌드 컨텍스트를 휴대 가능하고 버전이 지정된 도커 이미지로 변환하는 메커니즘입니다. 전체 프로세스는 도커 데몬(Docker Daemon)에 의해 관리되며, 계층화되고 불변하는 캐싱 시스템에 크게 의존합니다.


1. 빌드 아키텍처: 클라이언트 vs. 데몬

빌드 프로세스는 근본적으로 클라이언트-서버 작업입니다.

핵심 단계: 컨텍스트 업로드

docker build의 첫 번째 작업은 빌드 디렉터리(컨텍스트)의 모든 파일을 수집하여 데몬에게 전송하는 것입니다. 이것이 불필요한 파일(.dockerignore 파일로 제어)을 제외하고 최소한의 빌드 컨텍스트를 유지하는 것이 중요한 이유입니다. 크고 불필요한 파일을 전송하는 것은 캐싱 여부와 관계없이 모든 빌드의 시작 시간을 상당히 지연시킵니다.


2. 핵심 메커니즘: 불변 레이어와 캐싱

도커 이미지의 특징은 읽기 전용 레이어 스택으로 구축된다는 것입니다. Dockerfile의 각 명령어는 레이어를 생성합니다.

레이어 생성

빌드 캐시 (속도 요소)

도커 데몬은 어떤 명령어를 실행하기 전에, 과거에 실행된 정확히 동일한 명령어 로 생성된 기존 레이어가 로컬 캐시에 있는지 확인합니다.

  1. 캐시 히트(Cache Hit): 명령어 해당 명령어가 참조하는 모든 파일이 변경되지 않았다면, 데몬은 기존 레이어를 재사용하고 해당 명령어의 실행을 건너뜁니다. 이는 후속 빌드를 매우 빠르게 만듭니다.
  2. 캐시 무효화 (Cache Invalidation): 명령어 자체가 변경되거나, ADD 또는 COPY가 참조하는 파일 내용 이 변경되는 순간, 해당 단계의 캐시가 무효화됩니다. 더 중요한 것은, 캐시는 Dockerfile의 모든 후속 단계 에 대해서도 무효화된다는 것입니다.

이는 효율적인 docker build가 명령 순서를 올바르게 지정하여 캐시 히트를 최대화하는 데 전적으로 의존한다는 것을 의미합니다.


3. 단계별 명령어 실행

FROM (기반)

RUN (실행기)

# Good: 단 하나의 레이어만 생성
RUN apt-get update && \
	apt-get install -y git && \
	rm -rf /var/lib/apt/lists/*

COPY / ADD (파일 전송)

# 종속성만 복사하여 npm install 캐시 히트 가능성을 최대화
COPY package.json package-lock.json ./
RUN npm install
# 이 COPY는 소스 코드만 변경될 때 실행됨 (종속성이 변경될 때는 실행되지 않음):
COPY . .

CMD & ENTRYPOINT (시작 메타데이터)


4. 고급 최적화: 멀티 스테이지 빌드

멀티 스테이지 빌드는 최종 이미지 크기를 작게 유지하고 보안을 개선하는 모범 사례입니다.

# Stage 1: 빌드 (컴파일러 및 도구 포함)
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o /usr/local/bin/app ./cmd/server

# Stage 2: 런타임 (최소 크기)
FROM alpine:latest
# 'builder' 단계에서 최종 실행 파일만 복사
COPY --from=builder /usr/local/bin/app /usr/local/bin/app
ENTRYPOINT ["app"]

5. 모범 사례 요약

규칙 이유
.dockerignore 사용 불필요한 파일을 데몬에 보내지 않아 초기 컨텍스트 업로드 속도를 높입니다.
변경 빈도가 낮은 순서대로 레이어 정렬 RUN apt-get update와 같은 명령어는 캐시 히트를 최대화하기 위해 초기에 배치되어야 합니다. 소스 코드 COPY는 거의 끝에 위치해야 합니다.
RUN 명령어 연결 생성되는 총 레이어 수를 줄여 이미지를 더 작고 빠르게 가져올 수 있습니다.
멀티 스테이지 빌드 사용 개발 종속성을 제거하여 최종 이미지 크기와 공격 표면을 획기적으로 줄입니다.
구체적인 태그 사용 반복 가능한 빌드를 보장하기 위해 FROMnode:latest 대신 node:20-alpine과 같은 특정 태그를 사용합니다.
ADD 대신 COPY 선호 COPY는 예측 가능한 동작을 가지며 더 안전하고 투명하다고 간주됩니다.

도커 이미지 최적화의 두 가지 가장 중요한 기술인 빌드 캐시(Build Cache)멀티 스테이지 빌드(Multi-Stage Builds) 에 대해 자세히 설명해 드립니다. 이 둘은 목적이 다릅니다. 캐시는 속도를 위한 것이고, 멀티 스테이지 빌드는 크기와 보안을 위한 것입니다.


1. 도커 빌드 캐시: 속도와 불변성 ⚡

도커 빌드 캐시는 이전에 성공적으로 빌드된 레이어를 재사용하여 중복 단계를 건너뛰도록 설계된 자동화 메커니즘입니다.

핵심 개념: 불변 레이어와 해싱

Dockerfile의 모든 명령어(예: FROM, RUN, COPY)는 새롭고 불변하며 읽기 전용인 레이어를 생성합니다. 도커는 이 레이어들을 식별하기 위해 콘텐츠 해싱을 사용합니다.

  1. 명령어 해싱: 도커는 명령어 텍스트(예: RUN apt-get install -y git)를 가져와 고유한 해시를 계산합니다.
  2. 컨텍스트 해싱: COPYADD 명령어의 경우, 도커는 복사되는 _소스 파일_의 콘텐츠를 기반으로 해시를 계산합니다.
  3. 캐시 히트: 도커는 어떤 줄을 실행하기 전에, 현재 줄과 명령어 해시 및 컨텍스트 해시가 일치하는 레이어가 로컬 저장소에 존재하는지 확인합니다. 이 레이어가 발견되면, 도커는 해당 명령을 다시 실행하는 대신 기존 레이어를 재사용합니다. 이는 거의 즉각적입니다.

캐시 무효화 (핵심 규칙)

캐시는 순차적으로 작동합니다. 도커가 캐시 미스를 유발하는 명령어를 만나는 순간, 해당 명령어의 캐시가 무효화되고, 이후의 모든 명령어에 대해서도 캐시가 무효화됩니다.

모범 사례: 레이어 순서 지정

캐시 히트를 최대화하기 위한 황금률은 가장 적게 변경되는 명령어Dockerfile의 상단에, 가장 자주 변경되는 명령어를 하단에 배치하는 것입니다.

위치 명령어 이유
상단 FROM, RUN apt update 기본 OS 업데이트 시에만 변경됩니다 (변경 빈도 낮음).
중단 RUN npm install / RUN go build 종속성이 변경될 때 변경됩니다 (변경 빈도 중간).
하단 COPY . . (소스 코드) 거의 모든 커밋에서 변경됩니다 (변경 빈도 매우 높음).

2. 멀티 스테이지 빌드: 크기와 보안 📦

멀티 스테이지 빌드는 단일 Dockerfile 내에서 여러 개의 FROM 문을 사용할 수 있게 하는 최신 모범 사례입니다. 이는 최종 이미지에서 "빌드 도구 비대화" 문제를 해결합니다.

"빌드 도구 비대화" 문제

많은 애플리케이션(예: Go, Java, React로 작성된 애플리케이션)은 최종 실행 파일이나 정적 아티팩트를 빌드하는 데에만 컴파일러, SDK, 개발 라이브러리와 같은 대용량 종속성을 필요로 합니다. 이들을 모두 포함하면 최종 이미지 크기가 불필요하게 커집니다.

해결책: 명명된 스테이지 사용

멀티 스테이지 빌드는 AS 키워드를 사용하여 임시 스테이지를 정의합니다. 최종적으로 필요한 아티팩트(실행 파일 또는 컴파일된 파일)만 최소한의 런타임 스테이지로 복사됩니다.

문법 및 워크플로우

  1. 스테이지 1: 빌더 (무거운 작업): 이 스테이지는 모든 빌드 종속성이 포함된 큰 기반 이미지를 사용합니다 (예: golang:1.21 또는 node:20-alpine).
# Stage 1: 'builder'로 명명
FROM golang:1.21 AS builder 
WORKDIR /app
COPY . .
# Go 애플리케이션을 단일 실행 파일로 컴파일
RUN go build -o /usr/local/bin/my-app ./cmd/server
  1. 스테이지 2: 런타임 (최소 아티팩트 전송): 이 스테이지는 작고 가벼운 기반 이미지(예: scratch, alpine 또는 -slim 버전)로 새로 시작합니다. 결정적으로, COPY --from 명령을 사용하여 이전 스테이지에서 필요한 아티팩트만 복사합니다.
# Stage 2: 런타임
FROM alpine:latest
WORKDIR /usr/local/bin
# 'builder' 스테이지에서 컴파일된 실행 파일 *만* 복사
COPY --from=builder /usr/local/bin/my-app . 
ENTRYPOINT ["/usr/local/bin/my-app"]

결합된 영향

이 두 기능의 조합은 강력합니다.

references